/*
Copyright © 2020 Romber Li <romber2001@gmail.com>

Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at

	http://www.apache.org/licenses/LICENSE-2.0

Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
package config

import (
	"fmt"
	"path/filepath"
	"strings"

	"github.com/asaskevich/govalidator"
	"github.com/pingcap/errors"
	"github.com/romberli/go-multierror"
	"github.com/romberli/go-util/common"
	"github.com/romberli/go-util/constant"
	"github.com/romberli/go-util/middleware/mysql"
	"github.com/romberli/log"
	"github.com/spf13/cast"
	"github.com/spf13/viper"

	"github.com/romberli/das/pkg/message"
)

var (
	ValidRoles                      = []string{ServerRole, ServerRole}
	ValidLogLevels                  = []string{"debug", "info", "warn", "warning", "error", "fatal"}
	ValidLogFormats                 = []string{"text", "json"}
	ValidServerRouterHTTPErrorCodes = []int{200, 500}
	ValidAlertSTMPFormat            = []string{AlertSMTPTextFormat, AlertSMTPHTMLFormat}
)

// SetDefaultConfig set default configuration, it is the lowest priority
func SetDefaultConfig(baseDir string) {
	// daemon
	viper.SetDefault(DaemonKey, DefaultDaemon)
	// role
	viper.SetDefault(RoleKey, DefaultRole)
	// log
	defaultLogFile := filepath.Join(baseDir, DefaultLogDir, log.DefaultLogFileName)
	viper.SetDefault(LogFileKey, defaultLogFile)
	viper.SetDefault(LogLevelKey, log.DefaultLogLevel)
	viper.SetDefault(LogFormatKey, log.DefaultLogFormat)
	viper.SetDefault(LogMaxSizeKey, log.DefaultLogMaxSize)
	viper.SetDefault(LogMaxDaysKey, log.DefaultLogMaxDays)
	viper.SetDefault(LogMaxBackupsKey, log.DefaultLogMaxBackups)
	viper.SetDefault(LogRotateOnStartupKey, DefaultRotateOnStartup)
	viper.SetDefault(LogStdoutKey, DefaultLogStdout)
	// server
	viper.SetDefault(ServerAddrKey, DefaultServerAddr)
	defaultPidFile := filepath.Join(baseDir, fmt.Sprintf("%s.pid", DefaultCommandName))
	viper.SetDefault(ServerPidFileKey, defaultPidFile)
	viper.SetDefault(ServerReadTimeoutKey, DefaultServerReadTimeout)
	viper.SetDefault(ServerWriteTimeoutKey, DefaultServerWriteTimeout)
	viper.SetDefault(ServerPProfEnabledKey, DefaultServerPProfEnabled)
	viper.SetDefault(ServerDeploymentAccessibleZonesKey, DefaultServerDeploymentAccessibleZones)
	viper.SetDefault(ServerCallbackAddrKey, constant.EmptyString)
	viper.SetDefault(ServerRouterAlternativeBasePathKey, DefaultServerRouterAlternativeBasePath)
	viper.SetDefault(ServerRouterAlternativeBodyPathKey, DefaultServerRouterAlternativeBodyPath)
	viper.SetDefault(ServerRouterHTTPErrorCodeKey, DefaultServerRouterHTTPErrorCode)
	// database
	viper.SetDefault(DBDASMySQLAddrKey, fmt.Sprintf("%s:%d", constant.DefaultLocalHostIP, constant.DefaultMySQLPort))
	viper.SetDefault(DBDASMySQLNameKey, DefaultDBName)
	viper.SetDefault(DBDASMySQLUserKey, DefaultDBUser)
	viper.SetDefault(DBDASMySQLPassKey, DefaultDBPass)
	viper.SetDefault(DBPoolMaxConnectionsKey, mysql.DefaultMaxConnections)
	viper.SetDefault(DBPoolInitConnectionsKey, mysql.DefaultInitConnections)
	viper.SetDefault(DBPoolMaxIdleConnectionsKey, mysql.DefaultMaxIdleConnections)
	viper.SetDefault(DBPoolMaxIdleTimeKey, mysql.DefaultMaxIdleTime)
	viper.SetDefault(DBPoolMaxWaitTimeKey, mysql.DefaultMaxWaitTime)
	viper.SetDefault(DBPoolMaxRetryCountKey, mysql.DefaultMaxRetryCount)
	viper.SetDefault(DBPoolKeepAliveIntervalKey, mysql.DefaultKeepAliveInterval)
	viper.SetDefault(DBMonitorPrometheusUserKey, DefaultDBMonitorPrometheusUser)
	viper.SetDefault(DBMonitorPrometheusPassKey, DefaultDBMonitorPrometheusPass)
	viper.SetDefault(DBMonitorClickhouseUserKey, DefaultDBMonitorClickhouseUser)
	viper.SetDefault(DBMonitorClickhousePassKey, DefaultDBMonitorClickhousePass)
	viper.SetDefault(DBMonitorMySQLUserKey, DefaultDBMonitorMySQLUser)
	viper.SetDefault(DBMonitorMySQLPassKey, DefaultDBMonitorMySQLPass)
	viper.SetDefault(DBApplicationMySQLUserKey, DefaultDBApplicationMySQLUser)
	viper.SetDefault(DBApplicationMySQLPassKey, DefaultDBApplicationMySQLPass)
	// privilege
	viper.SetDefault(PrivilegeEnabledKey, DefaultPrivilegeEnabled)
	viper.SetDefault(PrivilegeSpecialTokensKey, DefaultPrivilegeSpecialTokens)
	// metadata
	viper.SetDefault(MetadataTableAnalyzeMinRoleKey, DefaultMetadataTableAnalyzeMinRole)
	// alert
	viper.SetDefault(AlertSMTPEnabledKey, DefaultAlertSMTPEnabled)
	viper.SetDefault(AlertSMTPFormatKey, DefaultAlterSMTPFormat)
	viper.SetDefault(AlertSMTPURLKey, DefaultAlertSMTPURL)
	viper.SetDefault(AlertSMTPUserKey, DefaultAlertSMTPUser)
	viper.SetDefault(AlertSMTPPassKey, DefaultAlertSMTPPass)
	viper.SetDefault(AlertSMTPFromKey, DefaultAlertSMTPFrom)
	viper.SetDefault(AlertHTTPEnabledKey, DefaultAlertHTTPEnabled)
	viper.SetDefault(AlertHTTPURLKey, DefaultAlertHTTPURL)
	viper.SetDefault(AlertHTTPConfigKey, DefaultAlertHTTPConfig)
	// healthcheck
	viper.SetDefault(HealthCheckBucketCapacityKey, DefaultHealthCheckBucketCapacity)
	viper.SetDefault(HealthCheckBucketNumKey, DefaultHealthCheckBucketNum)
	viper.SetDefault(HealthCheckBucketIntervalKey, DefaultHealthCheckBucketInterval)
	viper.SetDefault(HealthCheckBucketMaxWaitTimeKey, DefaultHealthCheckBucketMaxWaitTime)
	viper.SetDefault(HealthCheckAllowConcurrentKey, DefaultHealthCheckAllowConcurrent)
	viper.SetDefault(HealthcheckMaxRangeKey, DefaultHealthCheckMaxRange)
	// query
	viper.SetDefault(QueryMinRowsExaminedKey, DefaultQueryMinRowsExamined)
	viper.SetDefault(QueryMinExecTimeKey, DefaultQueryMinExecTime)
	// sqladvisor
	viper.SetDefault(SQLAdvisorSoarBinKey, DefaultSQLAdvisorSoarBin)
	viper.SetDefault(SQLAdvisorSoarConfigKey, DefaultSQLAdvisorSoarConfig)
	viper.SetDefault(SQLAdvisorSoarTempDirKey, DefaultSQLAdvisorSoarTempDir)
	viper.SetDefault(SQLAdvisorSoarSamplingKey, false)
	viper.SetDefault(SQLAdvisorSoarProfilingKey, false)
	viper.SetDefault(SQLAdvisorSoarTraceKey, false)
	viper.SetDefault(SQLAdvisorSoarExplainKey, false)
}

// ValidateConfig validates if the configuration is valid
func ValidateConfig() (err error) {
	merr := &multierror.Error{}

	// validate daemon section
	err = ValidateDaemon()
	if err != nil {
		merr = multierror.Append(merr, err)
	}

	// validate role section
	err = ValidateRole()
	if err != nil {
		merr = multierror.Append(merr, err)
	}

	// validate log section
	err = ValidateLog()
	if err != nil {
		merr = multierror.Append(merr, err)
	}

	// validate server section
	err = ValidateServer()
	if err != nil {
		merr = multierror.Append(merr, err)
	}

	// validate database section
	err = ValidateDatabase()
	if err != nil {
		merr = multierror.Append(merr, err)
	}

	// validate privilege section
	err = ValidatePrivilege()
	if err != nil {
		merr = multierror.Append(merr, err)
	}

	// validate metadata section
	err = ValidateMetadata()
	if err != nil {
		merr = multierror.Append(merr, err)
	}

	// validate alert section
	err = ValidateAlert()
	if err != nil {
		merr = multierror.Append(merr, err)
	}

	// validate healthcheck section
	err = ValidateHealthcheck()
	if err != nil {
		merr = multierror.Append(merr, err)
	}

	// validate query section
	err = ValidateQuery()
	if err != nil {
		merr = multierror.Append(merr, err)
	}

	// validate sql advisor section
	err = ValidateSQLAdvisor()
	if err != nil {
		merr = multierror.Append(merr, err)
	}

	return merr.ErrorOrNil()
}

// ValidateDaemon validates if daemon section is valid
func ValidateDaemon() error {
	_, err := cast.ToBoolE(viper.Get(DaemonKey))

	return errors.Trace(err)
}

// ValidateRole validates if role section is valid
func ValidateRole() error {
	merr := &multierror.Error{}

	role, err := cast.ToStringE(viper.Get(RoleKey))
	if err != nil {
		return errors.Trace(err)
	} else {
		if !common.ElementInSlice(ValidRoles, role) {
			merr = multierror.Append(merr, message.NewMessage(message.ErrNotValidRole, role))
		}
	}

	return errors.Trace(err)
}

// ValidateLog validates if log section is valid.
func ValidateLog() error {
	merr := &multierror.Error{}

	// validate log.FileName
	logFileName, err := cast.ToStringE(viper.Get(LogFileKey))
	if err != nil {
		merr = multierror.Append(merr, errors.Trace(err))
	} else {
		logFileName = strings.TrimSpace(logFileName)
		if logFileName == constant.EmptyString {
			merr = multierror.Append(merr, message.NewMessage(message.ErrEmptyLogFileName))
		}

		isAbs := filepath.IsAbs(logFileName)
		if !isAbs {
			logFileName, err = filepath.Abs(logFileName)
			if err != nil {
				merr = multierror.Append(merr, errors.Trace(err))
			}
		}
		valid, _ := govalidator.IsFilePath(logFileName)
		if !valid {
			merr = multierror.Append(merr, message.NewMessage(message.ErrNotValidLogFileName, logFileName))
		}
	}

	// validate log.level
	logLevel, err := cast.ToStringE(viper.Get(LogLevelKey))
	if err != nil {
		merr = multierror.Append(merr, errors.Trace(err))
	} else {
		if !common.ElementInSlice(ValidLogLevels, logLevel) {
			merr = multierror.Append(merr, message.NewMessage(message.ErrNotValidLogLevel, logLevel))
		}
	}

	// validate log.format
	logFormat, err := cast.ToStringE(viper.Get(LogFormatKey))
	if err != nil {
		merr = multierror.Append(merr, errors.Trace(err))
	} else {
		if !common.ElementInSlice(ValidLogFormats, logFormat) {
			merr = multierror.Append(merr, message.NewMessage(message.ErrNotValidLogFormat, logFormat))
		}
	}

	// validate log.maxSize
	logMaxSize, err := cast.ToIntE(viper.Get(LogMaxSizeKey))
	if err != nil {
		merr = multierror.Append(merr, errors.Trace(err))
	} else if logMaxSize < MinLogMaxSize || logMaxSize > MaxLogMaxSize {
		merr = multierror.Append(merr, message.NewMessage(message.ErrNotValidLogMaxSize, MinLogMaxSize, MaxLogMaxSize, logMaxSize))
	}

	// validate log.maxDays
	logMaxDays, err := cast.ToIntE(viper.Get(LogMaxDaysKey))
	if err != nil {
		merr = multierror.Append(merr, errors.Trace(err))
	} else if logMaxDays < MinLogMaxDays || logMaxDays > MaxLogMaxDays {
		merr = multierror.Append(merr, message.NewMessage(message.ErrNotValidLogMaxDays, MinLogMaxDays, MaxLogMaxDays, logMaxDays))
	}

	// validate log.maxBackups
	logMaxBackups, err := cast.ToIntE(viper.Get(LogMaxBackupsKey))
	if err != nil {
		merr = multierror.Append(merr, errors.Trace(err))
	} else if logMaxBackups < MinLogMaxDays || logMaxBackups > MaxLogMaxDays {
		merr = multierror.Append(merr, message.NewMessage(message.ErrNotValidLogMaxBackups, MinLogMaxBackups, MaxLogMaxBackups, logMaxBackups))
	}

	// validate log.rotateOnStartup
	_, err = cast.ToBoolE(viper.Get(LogRotateOnStartupKey))
	if err != nil {
		merr = multierror.Append(merr, errors.Trace(err))
	}

	// validate log.stdout
	_, err = cast.ToBoolE(viper.Get(LogStdoutKey))
	if err != nil {
		merr = multierror.Append(merr, errors.Trace(err))
	}

	return merr.ErrorOrNil()
}

// ValidateServer validates if server section is valid
func ValidateServer() error {
	merr := &multierror.Error{}

	// validate server.addr
	serverAddr, err := cast.ToStringE(viper.Get(ServerAddrKey))
	if err != nil {
		merr = multierror.Append(merr, errors.Trace(err))
	} else {
		serverAddrList := strings.Split(serverAddr, constant.ColonString)

		switch len(serverAddrList) {
		case 2:
			port := serverAddrList[1]
			if !govalidator.IsPort(port) {
				merr = multierror.Append(merr, message.NewMessage(message.ErrNotValidServerPort, constant.MinPort, constant.MaxPort, port))
			}
			viper.Set(ServerPortKey, port)
		default:
			merr = multierror.Append(merr, message.NewMessage(message.ErrNotValidServerAddr, serverAddr))
		}
	}

	// validate server.pidFile
	serverPidFile, err := cast.ToStringE(viper.Get(ServerPidFileKey))
	if err != nil {
		merr = multierror.Append(merr, errors.Trace(err))
	} else {
		isAbs := filepath.IsAbs(serverPidFile)
		if !isAbs {
			serverPidFile, err = filepath.Abs(serverPidFile)
			if err != nil {
				merr = multierror.Append(merr, errors.Trace(err))
			}
		}
		ok, _ := govalidator.IsFilePath(serverPidFile)
		if !ok {
			merr = multierror.Append(merr, message.NewMessage(message.ErrNotValidPidFile, serverPidFile))
		}
	}

	// validate server.readTimeout
	serverReadTimeout, err := cast.ToIntE(viper.Get(ServerReadTimeoutKey))
	if err != nil {
		merr = multierror.Append(merr, errors.Trace(err))
	} else if serverReadTimeout < MinServerReadTimeout || serverReadTimeout > MaxServerReadTimeout {
		merr = multierror.Append(merr, message.NewMessage(message.ErrNotValidServerReadTimeout, MinServerReadTimeout, MaxServerWriteTimeout, serverReadTimeout))
	}

	// validate server.writeTimeout
	serverWriteTimeout, err := cast.ToIntE(viper.Get(ServerWriteTimeoutKey))
	if err != nil {
		merr = multierror.Append(merr, errors.Trace(err))
	} else if serverWriteTimeout < MinServerWriteTimeout || serverWriteTimeout > MaxServerWriteTimeout {
		merr = multierror.Append(merr, message.NewMessage(message.ErrNotValidServerWriteTimeout, MinServerWriteTimeout, MaxServerWriteTimeout, serverWriteTimeout))
	}

	// validate server.pprof.enabled
	_, err = cast.ToBoolE(viper.Get(ServerPProfEnabledKey))
	if err != nil {
		merr = multierror.Append(merr, errors.Trace(err))
	}

	// validate server.deployment.accessibleZones
	serverDeploymentAccessibleZones, err := cast.ToStringSliceE(viper.GetStringSlice(ServerDeploymentAccessibleZonesKey))
	if err != nil {
		merr = multierror.Append(merr, errors.Trace(err))
	} else {
		for _, zone := range serverDeploymentAccessibleZones {
			zoneList := strings.Split(zone, constant.DotString)
			if len(zoneList) != 3 {
				merr = multierror.Append(merr, message.NewMessage(message.ErrNodeValidServerDeploymentAccessibleZone, zone))
			}
		}
	}

	// validate server.callbackAddr
	serverCallbackAddr, err := cast.ToStringE(viper.Get(ServerCallbackAddrKey))
	if err != nil {
		merr = multierror.Append(merr, errors.Trace(err))
	} else if serverCallbackAddr != constant.EmptyString {
		serverCallbackAddrList := strings.Split(serverCallbackAddr, constant.ColonString)

		switch len(serverCallbackAddrList) {
		case 2:
			port := serverCallbackAddrList[1]
			if !govalidator.IsPort(port) {
				merr = multierror.Append(merr, message.NewMessage(message.ErrNotValidServerPort, constant.MinPort, constant.MaxPort, port))
			}
		default:
			merr = multierror.Append(merr, message.NewMessage(message.ErrNotValidServerCallbackAddr, serverCallbackAddr))
		}
	}

	// validate server.router.alternativeBaseURL
	serverRouterAlternativeBasePath, err := cast.ToStringE(viper.Get(ServerRouterAlternativeBasePathKey))
	if err != nil {
		merr = multierror.Append(merr, errors.Trace(err))
	} else if serverRouterAlternativeBasePath != constant.EmptyString && !strings.HasPrefix(serverRouterAlternativeBasePath, constant.SlashString) {
		merr = multierror.Append(merr, message.NewMessage(message.ErrNotValidServerRouterAlternativeBasePath, serverRouterAlternativeBasePath))
	}

	// validate server.router.bodyPath
	_, err = cast.ToStringE(viper.Get(ServerRouterAlternativeBodyPathKey))
	if err != nil {
		merr = multierror.Append(merr, errors.Trace(err))
	}

	// validate server.router.httpErrorCode
	serverRouterHttpErrorCode, err := cast.ToIntE(viper.Get(ServerRouterHTTPErrorCodeKey))
	if err != nil {
		merr = multierror.Append(merr, errors.Trace(err))
	} else {
		if !common.ElementInSlice(ValidServerRouterHTTPErrorCodes, serverRouterHttpErrorCode) {
			merr = multierror.Append(merr, message.NewMessage(message.ErrNotValidServerRouterHTTPErrorCode, serverRouterHttpErrorCode))
		}
	}

	return merr.ErrorOrNil()
}

// ValidateDatabase validates if database section is valid
func ValidateDatabase() error {
	merr := &multierror.Error{}

	// validate db.das.mysql.addr
	dbDASAddr, err := cast.ToStringE(viper.Get(DBDASMySQLAddrKey))
	if err != nil {
		merr = multierror.Append(merr, errors.Trace(err))
	}
	dasAddr := strings.Split(dbDASAddr, constant.ColonString)
	if len(dasAddr) != 2 {
		merr = multierror.Append(merr, message.NewMessage(message.ErrNotValidDBAddr, dbDASAddr))
	} else {
		if !govalidator.IsHost(dasAddr[0]) || !govalidator.IsPort(dasAddr[1]) {
			merr = multierror.Append(merr, message.NewMessage(message.ErrNotValidDBAddr, dbDASAddr))
		}
	}
	// validate db.das.mysql.name
	_, err = cast.ToStringE(viper.Get(DBDASMySQLNameKey))
	if err != nil {
		merr = multierror.Append(merr, errors.Trace(err))
	}
	// validate db.das.mysql.user
	_, err = cast.ToStringE(viper.Get(DBDASMySQLUserKey))
	if err != nil {
		merr = multierror.Append(merr, errors.Trace(err))
	}
	// validate db.das.mysql.pass
	_, err = cast.ToStringE(viper.Get(DBDASMySQLPassKey))
	if err != nil {
		merr = multierror.Append(merr, errors.Trace(err))
	}
	// validate db.pool.maxConnections
	maxConnections, err := cast.ToIntE(viper.Get(DBPoolMaxConnectionsKey))
	if err != nil {
		merr = multierror.Append(merr, errors.Trace(err))
	} else if maxConnections < MinDBPoolMaxConnections || maxConnections > MaxDBPoolMaxConnections {
		merr = multierror.Append(merr, message.NewMessage(message.ErrNotValidDBPoolMaxConnections, MinDBPoolMaxConnections, MaxDBPoolMaxConnections, maxConnections))
	}
	// validate db.pool.initConnections
	initConnections, err := cast.ToIntE(viper.Get(DBPoolInitConnectionsKey))
	if err != nil {
		merr = multierror.Append(merr, errors.Trace(err))
	} else if initConnections < MinDBPoolInitConnections || initConnections > MaxDBPoolInitConnections {
		merr = multierror.Append(merr, message.NewMessage(message.ErrNotValidDBPoolInitConnections, MinDBPoolInitConnections, MaxDBPoolInitConnections, initConnections))
	}
	// validate db.pool.maxIdleConnections
	maxIdleConnections, err := cast.ToIntE(viper.Get(DBPoolMaxIdleConnectionsKey))
	if err != nil {
		merr = multierror.Append(merr, errors.Trace(err))
	} else if maxIdleConnections < MinDBPoolMaxIdleConnections || maxIdleConnections > MaxDBPoolMaxIdleConnections {
		merr = multierror.Append(merr, message.NewMessage(message.ErrNotValidDBPoolMaxIdleConnections, MinDBPoolMaxIdleConnections, MaxDBPoolMaxIdleConnections, maxIdleConnections))
	}
	// validate db.pool.maxIdleTime
	maxIdleTime, err := cast.ToIntE(viper.Get(DBPoolMaxIdleTimeKey))
	if err != nil {
		merr = multierror.Append(merr, errors.Trace(err))
	} else if maxIdleTime < MinDBPoolMaxIdleTime || maxIdleTime > MaxDBPoolMaxIdleTime {
		merr = multierror.Append(merr, message.NewMessage(message.ErrNotValidDBPoolMaxIdleTime, MinDBPoolMaxIdleTime, MaxDBPoolMaxIdleTime, maxIdleTime))
	}
	// validate db.pool.maxWaitTime
	maxWaitTime, err := cast.ToIntE(viper.Get(DBPoolMaxWaitTimeKey))
	if err != nil {
		merr = multierror.Append(merr, errors.Trace(err))
	} else if maxWaitTime < MinDBPoolMaxWaitTime || maxWaitTime > MaxDBPoolMaxWaitTime {
		merr = multierror.Append(merr, message.NewMessage(message.ErrNotValidDBPoolMaxWaitTime, MinDBPoolMaxWaitTime, MaxDBPoolMaxWaitTime, maxWaitTime))
	}
	// validate db.pool.maxRetryCount
	maxRetryCount, err := cast.ToIntE(viper.Get(DBPoolMaxRetryCountKey))
	if err != nil {
		merr = multierror.Append(merr, errors.Trace(err))
	} else if maxRetryCount < MinDBPoolMaxRetryCount || maxRetryCount > MaxDBPoolMaxRetryCount {
		merr = multierror.Append(merr, message.NewMessage(message.ErrNotValidDBPoolMaxRetryCount, MinDBPoolMaxRetryCount, MaxDBPoolMaxRetryCount, maxRetryCount))
	}
	// validate db.pool.keepAliveInterval
	keepAliveInterval, err := cast.ToIntE(viper.Get(DBPoolKeepAliveIntervalKey))
	if err != nil {
		merr = multierror.Append(merr, errors.Trace(err))
	} else if keepAliveInterval < MinDBPoolKeepAliveInterval || keepAliveInterval > MaxDBPoolKeepAliveInterval {
		merr = multierror.Append(merr, message.NewMessage(message.ErrNotValidDBPoolKeepAliveInterval, MinDBPoolKeepAliveInterval, MaxDBPoolKeepAliveInterval, keepAliveInterval))
	}
	// validate db.application.mysql.user
	_, err = cast.ToStringE(viper.Get(DBApplicationMySQLUserKey))
	if err != nil {
		merr = multierror.Append(merr, errors.Trace(err))
	}
	// validate db.application.mysql.pass
	_, err = cast.ToStringE(viper.Get(DBApplicationMySQLPassKey))
	if err != nil {
		merr = multierror.Append(merr, errors.Trace(err))
	}
	// validate db.monitor.prometheus.user
	_, err = cast.ToStringE(viper.Get(DBMonitorPrometheusUserKey))
	if err != nil {
		merr = multierror.Append(merr, errors.Trace(err))
	}
	// validate db.monitor.prometheus.pass
	_, err = cast.ToStringE(viper.Get(DBMonitorPrometheusPassKey))
	if err != nil {
		merr = multierror.Append(merr, errors.Trace(err))
	}
	// validate db.monitor.clickhouse.user
	_, err = cast.ToStringE(viper.Get(DBMonitorClickhouseUserKey))
	if err != nil {
		merr = multierror.Append(merr, errors.Trace(err))
	}
	// validate db.monitor.clickhouse.pass
	_, err = cast.ToStringE(viper.Get(DBMonitorClickhousePassKey))
	if err != nil {
		merr = multierror.Append(merr, errors.Trace(err))
	}
	// validate db.monitor.mysql.user
	_, err = cast.ToStringE(viper.Get(DBMonitorMySQLUserKey))
	if err != nil {
		merr = multierror.Append(merr, errors.Trace(err))
	}
	// validate db.monitor.mysql.pass
	_, err = cast.ToStringE(viper.Get(DBMonitorMySQLPassKey))
	if err != nil {
		merr = multierror.Append(merr, errors.Trace(err))
	}

	return errors.Trace(merr.ErrorOrNil())
}

// ValidatePrivilege validates if privilege section is valid
func ValidatePrivilege() error {
	merr := &multierror.Error{}

	// validate privilege.enabled
	_, err := cast.ToBoolE(viper.Get(PrivilegeEnabledKey))
	if err != nil {
		merr = multierror.Append(merr, errors.Trace(err))
	}
	// validate privilege.specialTokens
	_, err = cast.ToStringSliceE(viper.Get(PrivilegeSpecialTokensKey))
	if err != nil {
		merr = multierror.Append(merr, errors.Trace(err))
	}

	return errors.Trace(merr.ErrorOrNil())
}

// ValidateMetadata validates if metadata section is valid
func ValidateMetadata() error {
	merr := &multierror.Error{}

	// validate metadata.table.analyze.minRole
	minRole, err := cast.ToIntE(viper.Get(MetadataTableAnalyzeMinRoleKey))
	if err != nil {
		merr = multierror.Append(merr, errors.Trace(err))
	} else if minRole < MinMetadataTableAnalyzeMinRole || minRole > MaxMetadataTableAnalyzeMinRole {
		merr = multierror.Append(merr, message.NewMessage(message.ErrNotValidMetadataTableAnalyzeMinRole, MinMetadataTableAnalyzeMinRole, MaxMetadataTableAnalyzeMinRole, minRole))
	}

	return errors.Trace(merr.ErrorOrNil())
}

// ValidateAlert validates if alert section is valid
func ValidateAlert() error {
	merr := &multierror.Error{}

	// validate alert.smtp.enabled
	_, err := cast.ToBoolE(viper.Get(AlertSMTPEnabledKey))
	if err != nil {
		merr = multierror.Append(merr, errors.Trace(err))
	}
	// validate alert.smtp.format
	format, err := cast.ToStringE(viper.Get(AlertSMTPFormatKey))
	if err != nil {
		merr = multierror.Append(merr, errors.Trace(err))
	} else {
		if !common.ElementInSlice(ValidAlertSTMPFormat, format) {
			merr = multierror.Append(merr, message.NewMessage(message.ErrNotValidAlertSMTPFormat, format))
		}
	}

	// validate alert.smtp.addr
	_, err = cast.ToStringE(viper.Get(AlertSMTPURLKey))
	if err != nil {
		merr = multierror.Append(merr, errors.Trace(err))
	}
	// validate alert.smtp.user
	_, err = cast.ToStringE(viper.Get(AlertSMTPUserKey))
	if err != nil {
		merr = multierror.Append(merr, errors.Trace(err))
	}
	// validate alert.smtp.pass
	_, err = cast.ToStringE(viper.Get(AlertSMTPPassKey))
	if err != nil {
		merr = multierror.Append(merr, errors.Trace(err))
	}
	// validate alert.smtp.from
	_, err = cast.ToStringE(viper.Get(AlertSMTPFromKey))
	if err != nil {
		merr = multierror.Append(merr, errors.Trace(err))
	}

	// validate alert.http.enabled
	_, err = cast.ToBoolE(viper.Get(AlertHTTPEnabledKey))
	if err != nil {
		merr = multierror.Append(merr, errors.Trace(err))
	}
	// validate alert.http.url
	_, err = cast.ToStringE(viper.Get(AlertHTTPURLKey))
	if err != nil {
		merr = multierror.Append(merr, errors.Trace(err))
	}
	// validate alert.http.config
	_, err = cast.ToStringMapStringE(viper.Get(AlertHTTPConfigKey))
	if err != nil {
		merr = multierror.Append(merr, errors.Trace(err))
	}

	return errors.Trace(merr.ErrorOrNil())
}

// ValidateHealthcheck validates if health check section is valid
func ValidateHealthcheck() error {
	merr := &multierror.Error{}

	// validate healthcheck.bucket.capacity
	capacity, err := cast.ToIntE(viper.Get(HealthCheckBucketCapacityKey))
	if err != nil {
		merr = multierror.Append(merr, errors.Trace(err))
	} else {
		if capacity <= constant.ZeroInt {
			merr = multierror.Append(merr, message.NewMessage(message.ErrNotValidHealthcheckBucketCapacity, capacity))
		}
	}
	// validate healthcheck.bucket.num
	num, err := cast.ToIntE(viper.Get(HealthCheckBucketNumKey))
	if err != nil {
		merr = multierror.Append(merr, errors.Trace(err))
	} else {
		if num <= constant.ZeroInt {
			merr = multierror.Append(merr, message.NewMessage(message.ErrNotValidHealthcheckBucketNum, num))
		}
	}
	// validate healthcheck.bucket.interval
	interval, err := cast.ToIntE(viper.Get(HealthCheckBucketIntervalKey))
	if err != nil {
		merr = multierror.Append(merr, errors.Trace(err))
	} else {
		if interval <= constant.ZeroInt {
			merr = multierror.Append(merr, message.NewMessage(message.ErrNotValidHealthcheckBucketInterval, interval))
		}
	}
	// validate healthcheck.bucket.maxWaitTime
	maxWaitTime, err := cast.ToIntE(viper.Get(HealthCheckBucketMaxWaitTimeKey))
	if err != nil {
		merr = multierror.Append(merr, errors.Trace(err))
	} else {
		if maxWaitTime <= constant.ZeroInt {
			merr = multierror.Append(merr, message.NewMessage(message.ErrNotValidHealthcheckBucketMaxWaitTime, maxWaitTime))
		}
	}
	_, err = cast.ToBoolE(viper.Get(HealthCheckAllowConcurrentKey))
	if err != nil {
		merr = multierror.Append(merr, errors.Trace(err))
	}
	// validate healthcheck.maxRange
	_, err = cast.ToIntE(viper.Get(HealthcheckMaxRangeKey))
	if err != nil {
		merr = multierror.Append(merr, errors.Trace(err))
	}

	return errors.Trace(merr.ErrorOrNil())
}

// ValidateQuery validates if query section is valid
func ValidateQuery() error {
	merr := &multierror.Error{}

	// validate query.minRowsExamined
	_, err := cast.ToIntE(viper.Get(QueryMinRowsExaminedKey))
	if err != nil {
		merr = multierror.Append(merr, errors.Trace(err))
	}
	// validate query.minExecTime
	_, err = cast.ToFloat64E(viper.Get(QueryMinExecTimeKey))
	if err != nil {
		merr = multierror.Append(merr, errors.Trace(err))
	}

	return errors.Trace(merr.ErrorOrNil())
}

// ValidateSQLAdvisor validates if sql advisor section is valid
func ValidateSQLAdvisor() error {
	merr := &multierror.Error{}

	// validate sqladvisor.soar.bin
	soarBin, err := cast.ToStringE(viper.Get(SQLAdvisorSoarBinKey))
	if err != nil {
		merr = multierror.Append(merr, errors.Trace(err))
	} else {
		soarBin = strings.TrimSpace(soarBin)
		if soarBin == constant.EmptyString {
			merr = multierror.Append(merr, message.NewMessage(message.ErrEmptySoarBin))
		} else {
			isAbs := filepath.IsAbs(soarBin)
			if !isAbs {
				soarBin, err = filepath.Abs(soarBin)
				if err != nil {
					merr = multierror.Append(merr, errors.Trace(err))
				}
			}
			valid, _ := govalidator.IsFilePath(soarBin)
			if !valid {
				merr = multierror.Append(merr, message.NewMessage(message.ErrNotValidSoarBin, soarBin))
			}
		}
	}

	// validate sqladvisor.soar.config
	config, err := cast.ToStringE(viper.Get(SQLAdvisorSoarConfigKey))
	if err != nil {
		merr = multierror.Append(merr, errors.Trace(err))
	} else {
		config = strings.TrimSpace(config)
		if config == constant.EmptyString {
			merr = multierror.Append(merr, message.NewMessage(message.ErrEmptySoarConfig))
		} else {
			isAbs := filepath.IsAbs(config)
			if !isAbs {
				config, err = filepath.Abs(config)
				if err != nil {
					merr = multierror.Append(merr, errors.Trace(err))
				}
			}
			valid, _ := govalidator.IsFilePath(config)
			if !valid {
				merr = multierror.Append(merr, message.NewMessage(message.ErrNotValidSoarConfig, config))
			}
		}
	}

	// validate sqladvisor.soar.tempDir
	tempDir, err := cast.ToStringE(viper.Get(SQLAdvisorSoarTempDirKey))
	if err != nil {
		merr = multierror.Append(merr, errors.Trace(err))
	} else {
		if tempDir == constant.EmptyString {
			merr = multierror.Append(merr, message.NewMessage(message.ErrEmptySoarTempDir))
		} else {
			isAbs := filepath.IsAbs(tempDir)
			if !isAbs {
				tempDir, err = filepath.Abs(tempDir)
				if err != nil {
					merr = multierror.Append(merr, errors.Trace(err))
				}
			}
			valid, _ := govalidator.IsFilePath(tempDir)
			if !valid {
				merr = multierror.Append(merr, message.NewMessage(message.ErrNotValidSoarTempDir, tempDir))
			}
		}
	}

	// validate sqladvisor.soar.sampling
	_, err = cast.ToBoolE(viper.Get(SQLAdvisorSoarSamplingKey))
	if err != nil {
		merr = multierror.Append(merr, errors.Trace(err))
	}
	// validate sqladvisor.soar.profiling
	_, err = cast.ToBoolE(viper.Get(SQLAdvisorSoarProfilingKey))
	if err != nil {
		merr = multierror.Append(merr, errors.Trace(err))
	}
	// validate sqladvisor.soar.trace
	_, err = cast.ToBoolE(viper.Get(SQLAdvisorSoarTraceKey))
	if err != nil {
		merr = multierror.Append(merr, errors.Trace(err))
	}
	// validate sqladvisor.soar.explain
	_, err = cast.ToBoolE(viper.Get(SQLAdvisorSoarExplainKey))
	if err != nil {
		merr = multierror.Append(merr, errors.Trace(err))
	}

	return errors.Trace(merr.ErrorOrNil())
}

// TrimSpaceOfArg trims spaces of given argument
func TrimSpaceOfArg(arg string) string {
	args := strings.SplitN(arg, constant.EqualString, 2)

	switch len(args) {
	case 1:
		return strings.TrimSpace(args[0])
	case 2:
		argName := strings.TrimSpace(args[0])
		argValue := strings.TrimSpace(args[1])
		return fmt.Sprintf("%s=%s", argName, argValue)
	default:
		return arg
	}
}
